/*
 * Tranquil Java Integrated Development Environment
 *
 * The GNU General Public License Version 3
 *
 * Copyright (C) 2021 Autumn Lamonte
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @author Autumn Lamonte [AutumnWalksTheLake@gmail.com] ⚧ Trans Liberation Now
 * @version 1
 */
package tjide.ui;

import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;

import gjexer.TApplication;
import gjexer.TCommand;
import gjexer.THScroller;
import gjexer.TScrollableWindow;
import gjexer.TVScroller;
import gjexer.TWidget;
import gjexer.bits.CellAttributes;
import gjexer.event.TCommandEvent;
import gjexer.event.TKeypressEvent;
import gjexer.event.TMouseEvent;
import gjexer.event.TResizeEvent;
import static gjexer.TCommand.*;
import static gjexer.TKeypress.*;

import tjide.project.Project;

/**
 * WatchWindow is a permanent window that shows a list of watches.
 */
public class WatchWindow extends TScrollableWindow {

    /**
     * Translated strings.
     */
    private static ResourceBundle i18n = ResourceBundle.getBundle(WatchWindow.class.getName());

    // ------------------------------------------------------------------------
    // Constants --------------------------------------------------------------
    // ------------------------------------------------------------------------

    private static final TCommand cmEditWatch           = new TCommand(1010);
    private static final TCommand cmAddWatch            = new TCommand(1011);
    private static final TCommand cmDeleteWatch         = new TCommand(1012);

    /**
     * The number of lines to scroll on mouse wheel up/down.
     */
    private static final int wheelScrollSize = 3;

    // ------------------------------------------------------------------------
    // Variables --------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * The watches to display.
     */
    private List<Watch> watches = new ArrayList<Watch>();

    // ------------------------------------------------------------------------
    // Constructors -----------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Construct window.
     *
     * @param parent the main application
     */
    public WatchWindow(final TApplication parent) {
        super(parent, i18n.getString("windowTitle"), 0,
            parent.getDesktopBottom() - 11,
            parent.getScreen().getWidth(), 10,
            RESIZABLE | ABSOLUTEXY | HIDEONCLOSE);

        hScroller = new THScroller(this, 17, getHeight() - 2, getWidth() - 20);
        vScroller = new TVScroller(this, getWidth() - 2, 0, getHeight() - 2);
        setMinimumWindowWidth(25);
        setMinimumWindowHeight(10);
        setTopValue(0);
        setBottomValue(0);
        setLeftValue(0);
        setRightValue(getMaxWatchLength() - 1);

        statusBar = newStatusBar(i18n.getString("statusBar"));
        statusBar.addShortcutKeypress(kbF1, cmHelp,
            i18n.getString("statusBarHelp"));
        statusBar.addShortcutKeypress(kbEnter, cmEditWatch,
            i18n.getString("statusBarEdit"));
        statusBar.addShortcutKeypress(kbIns, cmAddWatch,
            i18n.getString("statusBarAdd"));
        statusBar.addShortcutKeypress(kbDel, cmDeleteWatch,
            i18n.getString("statusBarDelete"));
        statusBar.addShortcutKeypress(kbF10, cmMenu,
            i18n.getString("statusBarMenu"));

        hide();
    }

    // ------------------------------------------------------------------------
    // Event handlers ---------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Handle window/screen resize events.
     *
     * @param event resize event
     */
    @Override
    public void onResize(final TResizeEvent event) {
        if (event.getType() == TResizeEvent.Type.WIDGET) {
            // Resize the watches
            for (Watch watch: watches) {
                watch.setWidth(getWidth() - 2);
            }

            // Have TScrollableWindow handle the scrollbars
            super.onResize(event);
            return;
        }

        // Pass to children instead
        for (TWidget widget: getChildren()) {
            widget.onResize(event);
        }
    }

    /**
     * Handle mouse press events.
     *
     * @param mouse mouse button press event
     */
    @Override
    public void onMouseDown(final TMouseEvent mouse) {
        super.onMouseDown(mouse);

        if (mouse.isMouseWheelUp()) {
            for (int i = 0; i < wheelScrollSize; i++) {
                verticalDecrement();
            }
            alignWatches();
        } else if (mouse.isMouseWheelDown()) {
            for (int i = 0; i < wheelScrollSize; i++) {
                verticalIncrement();
            }
            alignWatches();
        }

        // User clicked on a watch, update the scrollbar accordingly.
        for (int i = 0; i < watches.size(); i++) {
            if (watches.get(i).isActive()) {
                setVerticalValue(i);
                return;
            }
        }
    }

    /**
     * Handle mouse release events.
     *
     * @param mouse mouse button release event
     */
    @Override
    public void onMouseUp(final TMouseEvent mouse) {
        // Use TWidget's code to pass the event to the children.
        super.onMouseUp(mouse);

        if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
            // Clicked/dragged on vertical scrollbar
            alignWatches();
        }
        if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
            // Clicked/dragged on horizontal scrollbar
            for (Watch watch: watches) {
                watch.setLeft(getHorizontalValue());
            }
        }

        // User clicked on a watch, update the scrollbar accordingly.
        for (int i = 0; i < watches.size(); i++) {
            if (watches.get(i).isActive()) {
                setVerticalValue(i);
                return;
            }
        }
    }

    /**
     * Method that subclasses can override to handle mouse movements.
     *
     * @param mouse mouse motion event
     */
    @Override
    public void onMouseMotion(final TMouseEvent mouse) {
        // Use TWidget's code to pass the event to the children.
        super.onMouseMotion(mouse);

        if (mouse.isMouse1() && mouseOnVerticalScroller(mouse)) {
            // Clicked/dragged on vertical scrollbar
            alignWatches();
        }
        if (mouse.isMouse1() && mouseOnHorizontalScroller(mouse)) {
            // Clicked/dragged on horizontal scrollbar
            for (Watch watch: watches) {
                watch.setLeft(getHorizontalValue());
            }
        }
    }

    /**
     * Handle keystrokes.
     *
     * @param keypress keystroke event
     */
    @Override
    public void onKeypress(final TKeypressEvent keypress) {
        if (keypress.equals(kbRight)) {
            horizontalIncrement();
            for (Watch watch: watches) {
                watch.setLeft(getHorizontalValue());
            }
            return;
        }
        if (keypress.equals(kbLeft)) {
            horizontalDecrement();
            for (Watch watch: watches) {
                watch.setLeft(getHorizontalValue());
            }
            return;
        }

        if (keypress.equals(kbDown)) {
            verticalIncrement();
            alignWatches();
            return;
        }
        if (keypress.equals(kbPgDn)) {
            bigVerticalIncrement();
            alignWatches();
            return;
        }
        if (keypress.equals(kbUp)) {
            verticalDecrement();
            alignWatches();
            return;
        }
        if (keypress.equals(kbPgUp)) {
            bigVerticalDecrement();
            alignWatches();
            return;
        }

        if (keypress.equals(kbEnter)) {
            // Edit current watch
            editWatch();
            return;
        }

        if (keypress.equals(kbIns)) {
            // Add watch
            addWatch();
            return;
        }

        if (keypress.equals(kbDel)) {
            // Delete watch
            deleteWatch();
            return;
        }

        // Pass it on
        super.onKeypress(keypress);
    }

    /**
     * Handle posted command events.
     *
     * @param command command event
     */
    @Override
    public void onCommand(final TCommandEvent command) {
        if (command.equals(cmEditWatch)) {
            editWatch();
        } else if (command.equals(cmAddWatch)) {
            addWatch();
        } else if (command.equals(cmDeleteWatch)) {
            deleteWatch();
        } else {
            // I didn't take it, pass it on.
            super.onCommand(command);
        }
    }

    // ------------------------------------------------------------------------
    // TWindow ----------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Retrieve the background color.
     *
     * @return the background color
     */
    @Override
    public final CellAttributes getBackground() {
        return getTheme().getColor("watchWindow.background");
    }

    /**
     * Retrieve the border color.
     *
     * @return the border color
     */
    @Override
    public CellAttributes getBorder() {
        if (inWindowMove) {
            return getTheme().getColor("watchWindow.windowMove");
        }
        return getTheme().getColor("watchWindow.background");
    }

    /**
     * Retrieve the color used by the window movement/sizing controls.
     *
     * @return the color used by the zoom box, resize bar, and close box
     */
    @Override
    public CellAttributes getBorderControls() {
        return getTheme().getColor("watchWindow.borderControls");
    }

    // ------------------------------------------------------------------------
    // TScrollableWindow ------------------------------------------------------
    // ------------------------------------------------------------------------

    // ------------------------------------------------------------------------
    // WatchWindow ------------------------------------------------------------
    // ------------------------------------------------------------------------

    /**
     * Make watches visible that match the vertical scrollbar.
     */
    private void alignWatches() {
        if (watches.size() == 0) {
            return;
        }

        int top = getVerticalValue();
        if (watches.size() <= getHeight() - 2) {
            // All watches fit in the window.
            for (int i = 0; i < watches.size(); i++) {
                watches.get(i).setEnabled(true);
                watches.get(i).setVisible(true);
                watches.get(i).setY(i);
            }
            activate(watches.get(top));
            return;
        }

        // Some watches will not fit in the window.
        int before = watches.size() - (getHeight() - 2);
        before -= (getBottomValue() - getVerticalValue());
        // System.err.println("BEFORE before " + before);
        int i = 0;
        for (; i < before; i++) {
            watches.get(i).setEnabled(false);
            watches.get(i).setVisible(false);
            watches.get(i).setY(-1);
            // System.err.println("BEFORE i " + i + " invisible");
        }
        for (int y = 0; (y < getHeight() - 2) && (i < watches.size());
             y++, i++) {

            watches.get(i).setEnabled(true);
            watches.get(i).setVisible(true);
            watches.get(i).setY(y);
            if (i == top) {
                activate(watches.get(i));
            }
            // System.err.println("i " + i + " VISIBLE y " + y);
        }
        for (; i < watches.size(); i++) {
            watches.get(i).setEnabled(false);
            watches.get(i).setVisible(false);
            watches.get(i).setY(-1);
            // System.err.println("AFTER i " + i + " invisible");
        }
    }

    /**
     * Get the maximum length of watches.
     *
     * @return the maximum length of watches
     */
    private int getMaxWatchLength() {
        int result = 0;
        for (Watch watch: watches) {
            if (watch.getDisplayLength() > result) {
                result = watch.getDisplayLength();
            }
        }
        return result;
    }

    /**
     * Add a new watch to this window.  Note package private access.
     *
     * @param expression the watch expression
     */
    void addWatch(final String expression) {
        Watch watch = new Watch(this, expression);
        watches.add(watch);
        setBottomValue(watches.size() - 1);
        setRightValue(getMaxWatchLength() - 1);
        setVerticalValue(getBottomValue());
        alignWatches();
        Project project = ((TranquilApplication) getApplication()).getProject();
        if (project != null) {
            List<String> watchStrings = new ArrayList<String>();
            for (Watch w: watches) {
                watchStrings.add(w.getExpression());
            }
            project.setWatches(watchStrings);
        }
    }

    /**
     * Update the selected watch.  Note package private access.
     *
     * @param expression the new watch expression
     */
    void updateWatch(final String expression) {
        Watch watch = watches.get(getVerticalValue());
        if (watch != null) {
            watch.setExpression(expression);
        }
        Project project = ((TranquilApplication) getApplication()).getProject();
        if (project != null) {
            List<String> watchStrings = new ArrayList<String>();
            for (Watch w: watches) {
                watchStrings.add(w.getExpression());
            }
            project.setWatches(watchStrings);
        }
    }

    /**
     * Delete the selected watch.
     */
    public void deleteWatch() {
        if (watches.size() == 0) {
            return;
        }

        Watch watch = watches.get(getVerticalValue());
        if (watch != null) {
            remove(watch, true);
            watches.remove(getVerticalValue());
        }
        if (watches.size() > 0) {
            setBottomValue(watches.size() - 1);
            setRightValue(getMaxWatchLength() - 1);
            if (getVerticalValue() > getBottomValue()) {
                setVerticalValue(getBottomValue());
            }
            alignWatches();
        } else {
            setVerticalValue(0);
            setBottomValue(0);
            setHorizontalValue(0);
            setRightValue(0);
        }

        Project project = ((TranquilApplication) getApplication()).getProject();
        if (project != null) {
            List<String> watchStrings = new ArrayList<String>();
            for (Watch w: watches) {
                watchStrings.add(w.getExpression());
            }
            project.setWatches(watchStrings);
        }
    }

    /**
     * Clear all watches in the window.
     */
    public void clearAll() {
        for (Watch watch: watches) {
            remove(watch, true);
        }
        watches.clear();
        setVerticalValue(0);
        setBottomValue(0);
        setHorizontalValue(0);
        setRightValue(0);
    }

    /**
     * Reset all watches to match given list.
     *
     * @param watchStrings watch expressions to load
     */
    public void loadAll(final List<String> watchStrings) {
        clearAll();

        for (String w: watchStrings) {
            Watch watch = new Watch(this, w);
            watches.add(watch);
        }
        setBottomValue(watches.size() - 1);
        setRightValue(getMaxWatchLength() - 1);
        setVerticalValue(getBottomValue());
        alignWatches();
    }

    /**
     * Edit selected watch.
     */
    public void editWatch() {
        if (watches.size() == 0) {
            return;
        }

        new NewWatchWindow(this, false,
            watches.get(getVerticalValue()).getExpression());
    }

    /**
     * Add new selected watch.
     */
    public void addWatch() {
        new NewWatchWindow(this, true, null);
    }

    /**
     * Update watches to reflect values at the current debug execution
     * location.
     */
    public void updateWatches() {
        for (Watch w: watches) {
            w.updateValue();
        }
    }

}
